package com.yanmaohu.nikita.scanner;

import com.yanmaohu.nikita.NikitaException;
import com.yanmaohu.nikita.configuration.Configuration;
import com.yanmaohu.nikita.utility.TokenUtility;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

/**
 * @author Bright Lee
 */
public final class Scanner {

    private final SourceCodeStream sourceCodeStream;
    private final List<Token> tokens;
    private int markedIndex;
    private int index;

    public Scanner(Configuration configuration, File file) {
        this.sourceCodeStream = new SourceCodeStream(configuration, file);
        this.tokens = new ArrayList<>();
        init();
    }

    public Token scan() {
        Token token;
        if (index >= tokens.size()) {
            token = tokens.get(tokens.size() - 1);
        } else {
            token = tokens.get(index++);
        }
        return token;
    }

    public File getFile() {
        return sourceCodeStream.getFile();
    }

    public SourceCode getCode() {
        return sourceCodeStream.getCode();
    }

    public int mark() {
        return markedIndex = index;
    }

    public void reset(int markedIndex) {
        index = markedIndex;
    }

    public void reset() {
        index = markedIndex;
    }

    private void init() {
        Token token;
        while (true) {
            token = scan0();
            if (token == null) {
                int index = sourceCodeStream.index();
                if (index >= sourceCodeStream.getCode().length()) {
                    int beginIndex = sourceCodeStream.getCode().length();
                    int endIndex = beginIndex;
                    token = new Token(TokenType.EOF, beginIndex, endIndex, null);
                    tokens.add(token);
                    break;
                } else {
                    continue;
                }
            } else {
                if (token.isGoto()) {
                    throw new NikitaException(getCode(), token.getBeginIndex(), token.getEndIndex(), "Goto is currently unavailable.");
                }
                tokens.add(token);
            }
        }
    }

    private Token scan0() {
        skipWhitespace();
        skipComment();
        skipWhitespace();
        Token token = scanIdentifier();
        if (token != null) {
            return token;
        }
        token = scanOperator();
        if (token != null) {
            return token;
        }
        token = scanInteger();
        if (token != null) {
            return token;
        }
        token = scanString();
        if (token != null) {
            return token;
        }
        token = scanChar();
        if (token != null) {
            return token;
        }
        int beginIndex = sourceCodeStream.index();
        int endIndex;
        int c;
        while (true) {
            sourceCodeStream.mark();
            c = sourceCodeStream.read();
            if (TokenUtility.isSeparator(c)) {
                sourceCodeStream.reset();
                endIndex = sourceCodeStream.index();
                break;
            }
        }
        if (c == -1) {
            return null;
        }
        throw new NikitaException(getCode(), beginIndex, endIndex, "Error token.");
    }

    private void skipWhitespace() {
        int c;
        while (true) {
            sourceCodeStream.mark();
            c = sourceCodeStream.read();
            if (TokenUtility.isWhitespace(c)) {
                continue;
            }
            sourceCodeStream.reset();
            break;
        }
    }

    private void skipComment() {
        loop : while (true) {
            skipWhitespace();
            int markedIndex = sourceCodeStream.mark();
            int c = sourceCodeStream.read();
            if (c == '/') {
                int c1 = sourceCodeStream.read();
                if (c1 == '/') {
                    while (true) {
                        int c2 = sourceCodeStream.read();
                        if (c2 < 0 || c2 == '\r' || c2 == '\n') {
                            continue loop;
                        }
                    }
                } else if (c1 == '*') {
                    while (true) {
                        int c2 = sourceCodeStream.read();
                        if (c2 < 0) {
                            continue loop;
                        }
                        if (c2 != '*') {
                            continue;
                        }
                        int c3 = sourceCodeStream.read();
                        if (c3 < 0 || c3 == '/') {
                            continue loop;
                        }
                    }
                } else {
                    sourceCodeStream.reset(markedIndex);
                    continue loop;
                }
            }
            sourceCodeStream.reset(markedIndex);
            break;
        }
    }

    private Token scanIdentifier() {
        int beginIndex = sourceCodeStream.index();
        int endIndex;
        int markedIndex = sourceCodeStream.mark();
        int c = sourceCodeStream.read();
        if (!TokenUtility.isIdentifierFirstChar(c)) {
            sourceCodeStream.reset(markedIndex);
            return null;
        }
        while (true) {
            sourceCodeStream.mark();
            c = sourceCodeStream.read();
            if (TokenUtility.isIdentifierTailChar(c)) {
                continue;
            }
            if (TokenUtility.isSeparator(c)) {
                sourceCodeStream.reset();
                endIndex = sourceCodeStream.index();
                String content = sourceCodeStream.getCode().substring(beginIndex, endIndex);
                TokenType tokenType = TokenType.getTokenType(content);
                if (tokenType == null) {
                    tokenType = TokenType.ID;
                }
                return new Token(tokenType, beginIndex, endIndex, content);
            }
            sourceCodeStream.reset(markedIndex);
            return null;
        }
    }

    public Token scanOperator() {
        int beginIndex = sourceCodeStream.index();
        int endIndex;
        int markedIndex = sourceCodeStream.mark();
        int c = sourceCodeStream.read();
        if (c == '!') {
            sourceCodeStream.mark();
            c = sourceCodeStream.read();
            if (c == '=') {
                endIndex = sourceCodeStream.index();
                return new Token(TokenType.NE, beginIndex, endIndex, TokenType.NE.content());
            }
            sourceCodeStream.reset();
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.NOT, beginIndex, endIndex, TokenType.NOT.content());
        }
        if (c == '%') {
            sourceCodeStream.mark();
            c = sourceCodeStream.read();
            if (c == '=') {
                endIndex = sourceCodeStream.index();
                return new Token(TokenType.REM_ASSIGN, beginIndex, endIndex, TokenType.REM_ASSIGN.content());
            }
            sourceCodeStream.reset();
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.REM, beginIndex, endIndex, TokenType.REM.content());
        }
        if (c == '&') {
            sourceCodeStream.mark();
            c = sourceCodeStream.read();
            if (c == '&') {
                sourceCodeStream.mark();
                c = sourceCodeStream.read();
                if (c == '=') {
                    endIndex = sourceCodeStream.index();
                    return new Token(TokenType.AND_ASSIGN, beginIndex, endIndex, TokenType.AND_ASSIGN.content());
                }
                sourceCodeStream.reset();
                endIndex = sourceCodeStream.index();
                return new Token(TokenType.AND, beginIndex, endIndex, TokenType.AND.content());
            }
            if (c == '=') {
                endIndex = sourceCodeStream.index();
                return new Token(TokenType.BIT_AND_ASSIGN, beginIndex, endIndex, TokenType.BIT_AND_ASSIGN.content());
            }
            sourceCodeStream.reset();
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.BIT_AND, beginIndex, endIndex, TokenType.BIT_AND.content());
        }
        if (c == '(') {
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.LEFT_PAR, beginIndex, endIndex, TokenType.LEFT_PAR.content());
        }
        if (c == ')') {
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.RIGHT_PAR, beginIndex, endIndex, TokenType.RIGHT_PAR.content());
        }
        if (c == '*') {
            sourceCodeStream.mark();
            c = sourceCodeStream.read();
            if (c == '=') {
                endIndex = sourceCodeStream.index();
                return new Token(TokenType.MUL_ASSIGN, beginIndex, endIndex, TokenType.MUL_ASSIGN.content());
            }
            sourceCodeStream.reset();
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.MUL, beginIndex, endIndex, TokenType.MUL.content());
        }
        if (c == '+') {
            sourceCodeStream.mark();
            c = sourceCodeStream.read();
            if (c == '+') {
                endIndex = sourceCodeStream.index();
                return new Token(TokenType.INC_ASSIGN, beginIndex, endIndex, TokenType.INC_ASSIGN.content());
            }
            if (c == '=') {
                endIndex = sourceCodeStream.index();
                return new Token(TokenType.ADD_ASSIGN, beginIndex, endIndex, TokenType.ADD_ASSIGN.content());
            }
            sourceCodeStream.reset();
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.PLUS, beginIndex, endIndex, TokenType.PLUS.content());
        }
        if (c == ',') {
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.COMMA, beginIndex, endIndex, TokenType.COMMA.content());
        }
        if (c == '-') {
            sourceCodeStream.mark();
            c = sourceCodeStream.read();
            if (c == '-') {
                endIndex = sourceCodeStream.index();
                return new Token(TokenType.DEC_ASSIGN, beginIndex, endIndex, TokenType.DEC_ASSIGN.content());
            }
            if (c == '=') {
                endIndex = sourceCodeStream.index();
                return new Token(TokenType.SUB_ASSIGN, beginIndex, endIndex, TokenType.SUB_ASSIGN.content());
            }
            sourceCodeStream.reset();
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.MINUS, beginIndex, endIndex, TokenType.MINUS.content());
        }
        if (c == '.') {
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.PERIOD, beginIndex, endIndex, TokenType.PERIOD.content());
        }
        if (c == '/') {
            sourceCodeStream.mark();
            c = sourceCodeStream.read();
            if (c == '=') {
                endIndex = sourceCodeStream.index();
                return new Token(TokenType.DIV_ASSIGN, beginIndex, endIndex, TokenType.DIV_ASSIGN.content());
            }
            sourceCodeStream.reset();
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.DIV, beginIndex, endIndex, TokenType.DIV.content());
        }
        if (c == ':') {
            sourceCodeStream.mark();
            c = sourceCodeStream.read();
            endIndex = sourceCodeStream.index();
            if (c == ':') {
                sourceCodeStream.mark();
                c = sourceCodeStream.read();
                endIndex = sourceCodeStream.index();
                if (c == ':') {
                    return new Token(TokenType.FORCE_TYPE_CONVERT, beginIndex, endIndex, TokenType.FORCE_TYPE_CONVERT.content());
                } else {
                    sourceCodeStream.reset();
                    return new Token(TokenType.TYPE_LIMIT, beginIndex, endIndex, TokenType.TYPE_LIMIT.content());
                }
            } else {
                sourceCodeStream.reset();
                return new Token(TokenType.COLON, beginIndex, endIndex, TokenType.COLON.content());
            }
        }
        if (c == ';') {
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.SEMICOLON, beginIndex, endIndex, TokenType.SEMICOLON.content());
        }
        if (c == '<') {
            sourceCodeStream.mark();
            c = sourceCodeStream.read();
            if (c == '=') {
                endIndex = sourceCodeStream.index();
                return new Token(TokenType.LE, beginIndex, endIndex, TokenType.LE.content());
            }
            if (c == '<') {
                endIndex = sourceCodeStream.index();
                return new Token(TokenType.LEFT_SHIFT, beginIndex, endIndex, TokenType.LEFT_SHIFT.content());
            }
            sourceCodeStream.reset();
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.LT, beginIndex, endIndex, TokenType.LT.content());
        }
        if (c == '=') {
            sourceCodeStream.mark();
            c = sourceCodeStream.read();
            if (c == '=') {
                endIndex = sourceCodeStream.index();
                return new Token(TokenType.EQ, beginIndex, endIndex, TokenType.EQ.content());
            }
            sourceCodeStream.reset();
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.ASSIGN, beginIndex, endIndex, TokenType.ASSIGN.content());
        }
        if (c == '>') {
            sourceCodeStream.mark();
            c = sourceCodeStream.read();
            if (c == '=') {
                endIndex = sourceCodeStream.index();
                return new Token(TokenType.GE, beginIndex, endIndex, TokenType.GE.content());
            }
            if (c == '>') {
                sourceCodeStream.mark();
                c = sourceCodeStream.read();
                if (c == '>') {
                    endIndex = sourceCodeStream.index();
                    return new Token(TokenType.AM_RIGHT_SHIFT, beginIndex, endIndex, TokenType.AM_RIGHT_SHIFT.content());
                }
                sourceCodeStream.reset();
                endIndex = sourceCodeStream.index();
                return new Token(TokenType.RIGHT_SHIFT, beginIndex, endIndex, TokenType.RIGHT_SHIFT.content());
            }
            sourceCodeStream.reset();
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.GT, beginIndex, endIndex, TokenType.GT.content());
        }
        if (c == '?') {
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.QUEST, beginIndex, endIndex, TokenType.QUEST.content());
        }
        if (c == '[') {
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.LEFT_BRK, beginIndex, endIndex, TokenType.LEFT_BRK.content());
        }
        if (c == ']') {
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.RIGHT_BRK, beginIndex, endIndex, TokenType.RIGHT_BRK.content());
        }
        if (c == '^') {
            sourceCodeStream.mark();
            c = sourceCodeStream.read();
            if (c == '=') {
                endIndex = sourceCodeStream.index();
                return new Token(TokenType.EXP_ASSIGN, beginIndex, endIndex, TokenType.EXP_ASSIGN.content());
            }
            sourceCodeStream.reset();
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.EXP, beginIndex, endIndex, TokenType.EXP.content());
        }
        if (c == '{') {
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.LEFT_BRC, beginIndex, endIndex, TokenType.LEFT_BRC.content());
        }
        if (c == '|') {
            sourceCodeStream.mark();
            c = sourceCodeStream.read();
            if (c == '|') {
                sourceCodeStream.mark();
                c = sourceCodeStream.read();
                if (c == '=') {
                    endIndex = sourceCodeStream.index();
                    return new Token(TokenType.OR_ASSIGN, beginIndex, endIndex, TokenType.OR_ASSIGN.content());
                }
                sourceCodeStream.reset();
                endIndex = sourceCodeStream.index();
                return new Token(TokenType.OR, beginIndex, endIndex, TokenType.OR.content());
            }
            if (c == '=') {
                endIndex = sourceCodeStream.index();
                return new Token(TokenType.BIT_OR_ASSIGN, beginIndex, endIndex, TokenType.BIT_OR_ASSIGN.content());
            }
            sourceCodeStream.reset();
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.BIT_OR, beginIndex, endIndex, TokenType.BIT_OR.content());
        }
        if (c == '}') {
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.RIGHT_BRC, beginIndex, endIndex, TokenType.RIGHT_BRC.content());
        }
        if (c == '~') {
            endIndex = sourceCodeStream.index();
            return new Token(TokenType.BIT_INVERT, beginIndex, endIndex, TokenType.BIT_INVERT.content());
        }
        sourceCodeStream.reset(markedIndex);
        return null;
    }

    public Token scanInteger() {
        int beginIndex = sourceCodeStream.index();
        int endIndex;
        String content;
        int markedIndex = sourceCodeStream.mark();
        int c = sourceCodeStream.read();
        if (!TokenUtility.isDigital(c)) {
            sourceCodeStream.reset();
            return null;
        }
        if (c == '0') {
            sourceCodeStream.mark();
            c = sourceCodeStream.read();
            if (c == 'x') {
                while (true) {
                    sourceCodeStream.mark();
                    c = sourceCodeStream.read();
                    if (TokenUtility.isDigital(c) || c >= 'A' && c <= 'F' || c >= 'a' && c <= 'f' || c == '_') {
                        continue;
                    }
                    break;
                }
                if (c == -1 || TokenUtility.isSeparator(c)) {
                    sourceCodeStream.reset();
                    endIndex = sourceCodeStream.index();
                    content = sourceCodeStream.getCode().substring(beginIndex, endIndex);
                    if (content.length() <= 2) {
                        throw new NikitaException(sourceCodeStream.getCode(), beginIndex, endIndex, "Illegal hexadecimal number.");
                    }
                    String value = content.substring(2).replaceAll("_", "");
                    int length = value.length();
                    if (length < 1) {
                        throw new NikitaException(sourceCodeStream.getCode(), beginIndex, endIndex, "Illegal hexadecimal number.");
                    }
                    return new Token(TokenType.INTEGER_LITERAL, beginIndex, endIndex, content);
                } else {
                    throw new NikitaException(sourceCodeStream.getCode(), beginIndex, sourceCodeStream.index(), "Illegal hexadecimal number.");
                }
            } else if (c == 'b') {
                while (true) {
                    sourceCodeStream.mark();
                    c = sourceCodeStream.read();
                    if (c == '0' || c == '1' || c == '_') {
                        continue;
                    }
                    break;
                }
                if (c == -1 || TokenUtility.isSeparator(c)) {
                    sourceCodeStream.reset();
                    endIndex = sourceCodeStream.index();
                    content = sourceCodeStream.getCode().substring(beginIndex, endIndex);
                    if (content.length() <= 2) {
                        throw new NikitaException(sourceCodeStream.getCode(), beginIndex, endIndex, "Illegal binary number.");
                    }
                    String value = content.substring(2).replaceAll("_", "");
                    int length = value.length();
                    if (length < 1) {
                        throw new NikitaException(sourceCodeStream.getCode(), beginIndex, endIndex, "Illegal binary number.");
                    }
                    return new Token(TokenType.INTEGER_LITERAL, beginIndex, endIndex, content);
                } else {
                    throw new NikitaException(sourceCodeStream.getCode(), beginIndex, sourceCodeStream.index(), "Illegal binary number.");
                }
            } else {
                sourceCodeStream.reset(markedIndex);
            }
        }
        int markedIndex1;
        while (true) {
            markedIndex1 = sourceCodeStream.mark();
            c = sourceCodeStream.read();
            if (!(TokenUtility.isDigital(c) || c == '_')) {
                break;
            }
        }
        if (TokenUtility.isSeparator(c)) {
            sourceCodeStream.reset(markedIndex1);
            endIndex = sourceCodeStream.index();
            content = sourceCodeStream.getCode().substring(beginIndex, endIndex);
            checkDecimalNo(beginIndex, endIndex, content);
            return new Token(TokenType.INTEGER_LITERAL, beginIndex, endIndex, content);
        }
        if (c == 'E' || c == 'e') {
            c = sourceCodeStream.read();
            if (TokenUtility.isSeparator(c)) {
                sourceCodeStream.reset(markedIndex1);
                endIndex = sourceCodeStream.index();
                content = sourceCodeStream.getCode().substring(beginIndex, endIndex);
                checkDecimalNo(beginIndex, endIndex, content);
                return new Token(TokenType.INTEGER_LITERAL, beginIndex, endIndex, content);
            }
        }
        sourceCodeStream.reset(markedIndex);
        return null;
    }

    private Token scanString() {
        int beginIndex = sourceCodeStream.index();
        int endIndex;
        String content;
        int markedIndex = sourceCodeStream.mark();
        int c = sourceCodeStream.read();
        if (c != '"') {
            sourceCodeStream.reset(markedIndex);
            return null;
        }
        while (true) {
            sourceCodeStream.mark();
            c = sourceCodeStream.read();
            if (c == '\n' || c == '\r') {
                sourceCodeStream.reset(markedIndex);
                return null;
            }
            if (c == '"') {
                endIndex = sourceCodeStream.index();
                content = sourceCodeStream.getCode().substring(beginIndex + 1, endIndex - 1);
                return new Token(TokenType.STRING_LITERAL, beginIndex, endIndex, content);
            }
            if (c == '\\') {
                sourceCodeStream.mark();
                c = sourceCodeStream.read();
                if (!TokenUtility.isEscapeChar(c)) {
                    sourceCodeStream.reset(markedIndex);
                    return null;
                }
            }
        }
    }

    private Token scanChar() {
        int beginIndex = sourceCodeStream.index();
        int endIndex;
        String content;
        int markedIndex = sourceCodeStream.mark();
        int c = sourceCodeStream.read();
        if (c != '\'') {
            sourceCodeStream.reset(markedIndex);
            return null;
        }
        sourceCodeStream.mark();
        c = sourceCodeStream.read();
        if (c == '\n' || c == '\r') {
            sourceCodeStream.reset(markedIndex);
            return null;
        }
        if (c == '\\') {
            sourceCodeStream.mark();
            c = sourceCodeStream.read();
            if (!TokenUtility.isEscapeChar(c)) {
                sourceCodeStream.reset(markedIndex);
                return null;
            }
        }
        sourceCodeStream.mark();
        c = sourceCodeStream.read();
        if (c != '\'') {
            sourceCodeStream.reset(markedIndex);
            return null;
        }
        endIndex = sourceCodeStream.index();
        content = sourceCodeStream.getCode().substring(beginIndex + 1, endIndex - 1);
        return new Token(TokenType.CHARACTER_LITERAL, beginIndex, endIndex, content);
    }

    private void checkDecimalNo(int beginIndex, int endIndex, String content) {
        content = content.toUpperCase();
        String x;
        String y;
        String z;
        if (content.contains(".")) {
            String[] a1 = content.split("\\.");
            x = a1[0];
            y = a1[1];
            if (y.contains("E")) {
                String[] a2 = y.split("E");
                y = a2[0];
                z = a2[1];
            } else {
                z = null;
            }
        } else if (content.contains("E")) {
            String[] a1 = content.split("E");
            x = a1[0];
            y = null;
            z = a1[1];
        } else {
            x = content;
            y = null;
            z = null;
        }
        if (x != null) {
            x = x.replaceAll("_", "");
            if (x.isEmpty()) {
                throw new NikitaException(getCode(), beginIndex, endIndex, "Illegal decimal number.");
            }
            for (int i = 0; i < x.length(); i++) {
                char c = x.charAt(i);
                if (!(TokenUtility.isDigital(c))) {
                    throw new NikitaException(getCode(), beginIndex, endIndex, "Illegal decimal number.");
                }
            }
        }
        if (y != null) {
            y = y.replaceAll("_", "");
            if (y.isEmpty()) {
                throw new NikitaException(getCode(), beginIndex, endIndex, "Illegal decimal number.");
            }
            for (int i = 0; i < y.length(); i++) {
                char c = y.charAt(i);
                if (!(TokenUtility.isDigital(c))) {
                    throw new NikitaException(getCode(), beginIndex, endIndex, "Illegal decimal number.");
                }
            }
        }
        if (z != null) {
            z = z.replaceAll("_", "");
            if (!z.isEmpty()) {
                char ch = z.charAt(0);
                if (ch == '+' || ch == '-') {
                    z = z.substring(1);
                }
            }
            if (z.isEmpty()) {
                throw new NikitaException(getCode(), beginIndex, endIndex, "Illegal decimal number.");
            }
            for (int i = 0; i < z.length(); i++) {
                char c = z.charAt(i);
                if (!(TokenUtility.isDigital(c))) {
                    throw new NikitaException(getCode(), beginIndex, endIndex, "Illegal decimal number.");
                }
            }
        }
    }

}
